Перейти к основному содержимому

5.16. Справочник по Lisp

Разработчику Архитектору Инженеру

Справочник по Lisp

Основы синтаксиса и базовые структуры данных

1. Синтаксис выражений

Все программы на Lisp записываются в виде S-выражений (symbolic expressions). S-выражение — это либо атом, либо список.

Примеры:

42               ; атом (число)
hello ; атом (символ)
"world" ; атом (строка)
(+ 1 2) ; список: вызов функции +

Выражение всегда вычисляется. Результат вычисления зависит от типа выражения.

2. Атомы

Атом — это неделимая единица данных. К атомам относятся:

  • Числа: целые, рациональные, вещественные, комплексные.
  • Символы: имена переменных, функций, ключевые слова.
  • Строки: последовательности символов в двойных кавычках.
  • Ключевые слова: символы, начинающиеся с двоеточия (:keyword), самоссылающиеся.
  • Логические значения: T (истина), NIL (ложь и пустой список одновременно).

Примеры:

123              ; целое число
3/4 ; рациональное число
3.14 ; вещественное число
#C(1 2) ; комплексное число 1+2i
my-var ; символ
"Hello" ; строка
:debug ; ключевое слово
T ; истина
NIL ; ложь / пустой список

3. Списки

Список — это упорядоченная последовательность элементов, заключённая в круглые скобки. Список может быть пустым: () или NIL.

Списки строятся из cons-ячеек — пар (car . cdr), где car — первый элемент, cdr — остальная часть списка.

Функции для работы со списками:

  • (cons a b) — создаёт cons-ячейку из a и b.
  • (car list) — возвращает первый элемент списка.
  • (cdr list) — возвращает список без первого элемента.
  • (list a b c ...) — создаёт новый список из аргументов.
  • (append list1 list2 ...) — объединяет списки.
  • (length list) — возвращает количество элементов.
  • (nth n list) — возвращает элемент под индексом n (с нуля).
  • (last list) — возвращает последний cons-элемент списка.
  • (reverse list) — возвращает список в обратном порядке.

Примеры:

(cons 1 '(2 3))        ; → (1 2 3)
(car '(a b c)) ; → A
(cdr '(a b c)) ; → (B C)
(list 1 2 3) ; → (1 2 3)
(append '(1 2) '(3 4)) ; → (1 2 3 4)
(nth 1 '(x y z)) ; → Y

4. Типы данных

Common Lisp поддерживает богатую систему типов. Основные категории:

4.1. Числовые типы

  • integer — целые числа (неограниченной точности).
  • ratio — рациональные числа (дроби, например 2/3).
  • float — вещественные числа (одинарной, двойной и других точностей).
  • complex — комплексные числа.

Функции:

  • (numberp x) — проверяет, является ли x числом.
  • (integerp x), (floatp x), (ratiop x), (complexp x) — проверки конкретных числовых типов.
  • (zerop x), (plusp x), (minusp x) — проверки знака.
  • (evenp x), (oddp x) — чётность/нечётность.

4.2. Символы

Символ — именованная сущность, которая может иметь:

  • имя,
  • значение (если используется как переменная),
  • функциональное определение (если используется как функция),
  • свойства (plist).

Функции:

  • (symbolp x) — проверка, является ли x символом.
  • (symbol-name sym) — имя символа как строка.
  • (boundp sym) — проверка, имеет ли символ значение.
  • (fboundp sym) — проверка, связана ли функция с символом.
  • (symbol-value sym) — получение значения символа.
  • (symbol-function sym) — получение функции, связанной с символом.

4.3. Строки

Строка — массив символов (character).

Функции:

  • (stringp x) — проверка, является ли x строкой.
  • (string= s1 s2) — сравнение строк (регистрозависимо).
  • (string-equal s1 s2) — сравнение без учёта регистра.
  • (length str) — длина строки.
  • (char str i) — символ по индексу i.
  • (subseq str start &optional end) — подстрока.
  • (concatenate 'string s1 s2 ...) — объединение строк.

4.4. Массивы и векторы

  • Вектор — одномерный массив.
  • Массивы могут быть многомерными.

Функции:

  • (vectorp x) — проверка вектора.
  • (aref array &rest indices) — доступ к элементу.
  • (make-array dimensions &key initial-element element-type fill-pointer adjustable) — создание массива.

4.5. Хэш-таблицы

Ассоциативные структуры данных.

Функции:

  • (make-hash-table &key test size rehash-size rehash-threshold) — создание.
  • (gethash key table &optional default) — получение значения.
  • (setf (gethash key table) value) — установка значения.
  • (remhash key table) — удаление ключа.
  • (clrhash table) — очистка таблицы.
  • (maphash function table) — применение функции к каждой паре ключ-значение.

4.6. Потоки (streams)

Используются для ввода/вывода.

Функции:

  • (open filename &key direction if-exists if-does-not-exist) — открытие файла.
  • (close stream) — закрытие.
  • (read stream) — чтение объекта.
  • (write object &key stream) — запись.
  • (print object &optional stream) — печать с новой строки.
  • (format destination control-string &rest args) — форматированный вывод.

Функции, лямбда-выражения, замыкания и рекурсия

1. Определение функций

Функции в Common Lisp определяются с помощью формы defun.

Синтаксис:

(defun имя (параметры)
тело...)

Пример:

(defun square (x)
(* x x))

Вызов:

(square 5)  ; → 25

Функция возвращает значение последнего вычисленного выражения в теле.

2. Параметры функций

Common Lisp поддерживает несколько видов параметров:

2.1. Обязательные параметры

Указываются просто по имени.

(defun add (a b) (+ a b))

2.2. Необязательные параметры (&optional)

Могут иметь значения по умолчанию.

(defun greet (name &optional (greeting "Hello"))
(format nil "~A, ~A!" greeting name))

Вызовы:

(greet "Alice")           ; → "Hello, Alice!"
(greet "Bob" "Hi") ; → "Hi, Bob!"

2.3. Остаточные параметры (&rest)

Собирают все оставшиеся аргументы в список.

(defun sum (&rest numbers)
(apply #'+ numbers))

Вызов:

(sum 1 2 3 4)  ; → 10

2.4. Именованные параметры (&key)

Аргументы передаются как пары ключ–значение.

(defun make-point (&key (x 0) (y 0))
(list x y))

Вызов:

(make-point :x 10 :y 20)  ; → (10 20)
(make-point :y 5) ; → (0 5)

Ключевые параметры могут быть обязательными через &key ((x x-supplied-p)), но чаще используют значения по умолчанию.

2.5. Вспомогательные параметры (&aux)

Позволяют объявлять локальные переменные внутри списка параметров.

(defun circle-area (r &aux (pi 3.14159))
(* pi r r))

Все виды параметров можно комбинировать в порядке: обязательные &optional необязательные &rest остаток &key ключи &aux вспомогательные

Пример:

(defun complex-fn (a b &optional (c 0) &rest rest &key (debug nil) &aux (sum (+ a b c)))
(when debug (print sum))
(list sum rest))

3. Лямбда-выражения

Анонимные функции создаются с помощью lambda.

Синтаксис:

(lambda (аргументы) тело...)

Пример:

(funcall (lambda (x) (* x x)) 7)  ; → 49

Лямбда-выражения часто используются как аргументы для функций высшего порядка.

4. Функции высшего порядка

Это функции, принимающие другие функции в качестве аргументов или возвращающие их.

Основные функции:

  • (funcall function &rest args) — вызывает функцию с аргументами.
  • (apply function arg-list) — вызывает функцию, раскрывая список аргументов.
  • (mapcar function list) — применяет функцию к каждому элементу списка, возвращает новый список.
  • (mapcan function list) — как mapcar, но результаты конкатенируются через nconc.
  • (every predicate list) — проверяет, выполняется ли предикат для всех элементов.
  • (some predicate list) — проверяет, выполняется ли предикат хотя бы для одного элемента.
  • (reduce function list &key initial-value) — сворачивает список бинарной операцией.

Примеры:

(mapcar #'square '(1 2 3 4))      ; → (1 4 9 16)
(every #'plusp '(1 2 3)) ; → T
(some #'minusp '(-1 2 3)) ; → T
(reduce #'+ '(1 2 3 4)) ; → 10
(apply #'+ '(1 2 3)) ; → 6
(funcall #'+ 1 2 3) ; → 6

Заметим: #' — это сокращение для (function ...), используется для ссылки на функцию по имени.

5. Замыкания

Замыкание — функция, захватывающая переменные из окружающей лексической области.

Пример:

(defun make-counter (initial)
(let ((count initial))
(lambda () (incf count))))

Использование:

(defvar counter (make-counter 10))
(funcall counter) ; → 11
(funcall counter) ; → 12

Переменная count сохраняется между вызовами благодаря замыканию.

6. Рекурсия

Lisp естественно поддерживает рекурсивные функции. Хвостовая рекурсия не гарантирует оптимизацию во всех реализациях, но многие компиляторы её поддерживают.

Пример факториала:

(defun factorial (n)
(if (<= n 1)
1
(* n (factorial (- n 1)))))

Рекурсия с аккумулятором (хвостовая):

(defun factorial-tail (n &optional (acc 1))
(if (<= n 1)
acc
(factorial-tail (- n 1) (* n acc))))

7. Локальные функции

Форма flet позволяет определять локальные функции, видимые только внутри блока.

Синтаксис:

(flet ((имя (параметры) тело) ...)
основное-тело)

Пример:

(flet ((square (x) (* x x))
(double (x) (* 2 x)))
(+ (square 3) (double 4))) ; → 9 + 8 = 17

Форма labels аналогична flet, но позволяет рекурсивные и взаимно рекурсивные определения, так как тела функций видят друг друга.

Пример взаимной рекурсии:

(labels ((even? (n)
(if (zerop n) t (odd? (- n 1))))
(odd? (n)
(if (zerop n) nil (even? (- n 1)))))
(even? 4)) ; → T

8. Компиляция функций

Функции могут быть скомпилированы для повышения производительности.

  • (compile 'имя) — компилирует уже определённую функцию.
  • (compile-file "файл.lisp") — компилирует весь файл в .fasl.
  • (load "файл.fasl") — загружает скомпилированный файл.

Форма defun в интерактивной среде обычно создаёт интерпретируемую функцию, но многие реализации (SBCL, CCL) автоматически компилируют.

9. Интроспекция функций

Common Lisp предоставляет средства для анализа функций:

  • (function-lambda-expression fn) — может вернуть исходное лямбда-выражение (не во всех реализациях).
  • (describe fn) — выводит информацию о функции.
  • (documentation 'имя 'function) — получает строку документации, если она указана.

Пример с документацией:

(defun add (a b)
"Возвращает сумму двух чисел."
(+ a b))

(documentation 'add 'function) ; → "Возвращает сумму двух чисел."

Специальные формы и макросы

1. Что такое специальные формы

Специальные формы — это встроенные конструкции языка, которые не вычисляют свои аргументы обычным образом. Они управляют потоком выполнения, привязками переменных, компиляцией и другими фундаментальными аспектами.

Common Lisp имеет небольшой набор специальных форм (всего около 25), но на их основе строятся все остальные управляющие конструкции через макросы.

2. Основные специальные формы

2.1. quote

Предотвращает вычисление выражения и возвращает его как данные.

Синтаксис:

(quote x)

Сокращение: 'x

Примеры:

(quote (+ 1 2))   ; → (+ 1 2)
'hello ; → HELLO
'(1 2 3) ; → (1 2 3)

2.2. setq

Присваивает значение переменной (или нескольким переменным).

Синтаксис:

(setq var1 value1 var2 value2 ...)

Пример:

(setq x 10 y 20)

setq работает с уже существующими переменными. Для глобальных переменных предпочтительно использовать defvar или defparameter.

2.3. let и let*

Создают локальные привязки переменных.

  • let — все инициализаторы вычисляются в параллельном окружении, то есть не видят друг друга.
  • let* — инициализаторы вычисляются последовательно, каждая последующая переменная видит предыдущие.

Примеры:

(let ((a 1) (b 2))
(+ a b)) ; → 3

(let* ((a 1) (b (+ a 1)))
(+ a b)) ; → 3

Без let* второй пример вызвал бы ошибку, если бы b зависел от a в let.

2.4. if

Условное ветвление с двумя ветками.

Синтаксис:

(if условие
форма-если-истина
форма-если-ложь)

Ветка «ложь» необязательна.

Пример:

(if (> x 0)
"positive"
"non-positive")

2.5. cond

Многоусловное ветвление.

Синтаксис:

(cond
(условие1 форма1...)
(условие2 форма2...)
...
(t форма-по-умолчанию...))

Каждая ветка — список, где первое выражение — условие, остальные — тело. Выполняется первая ветка с истинным условием.

Пример:

(cond
((< x 0) "negative")
((= x 0) "zero")
(t "positive"))

2.6. progn

Группирует несколько форм в одну. Возвращает значение последней формы.

Используется там, где синтаксис допускает только одну форму, но нужно выполнить несколько.

Пример:

(if (> x 0)
(progn
(print "Positive!")
(* x 2)))

2.7. block и return-from

block задаёт именованную точку выхода. return-from немедленно возвращает значение из блока.

Синтаксис:

(block имя
тело...)

Пример:

(block my-block
(when (< x 0) (return-from my-block "Invalid"))
(* x x))

Анонимный блок создаётся формой prog1, prog2, progn, но именованный даёт гибкость.

2.8. tagbody и go

Низкоуровневый механизм меток и безусловных переходов.

Пример:

(tagbody
start
(print "Hello")
(go end)
end
(print "Done"))

Редко используется напрямую, чаще служит основой для макросов циклов.

2.9. function

Ссылается на функциональное определение.

Синтаксис:

(function имя)

Сокращение: #'имя

Пример:

(mapcar #'sqrt '(4 9 16))  ; → (2 3 4)

2.10. eval

Вычисляет переданное S-выражение как код.

Пример:

(eval '(+ 1 2))  ; → 3

Использовать eval следует крайне редко — он медленный и небезопасный.

2.11. load и compile-file

  • (load "file") — загружает и выполняет файл.
  • (compile-file "file.lisp") — компилирует файл в машинный код (.fasl).

Эти формы управляют загрузкой кода на этапе выполнения.


3. Макросы

Макросы — ключевой инструмент метапрограммирования в Lisp. Они преобразуют код на этапе чтения/компиляции, до выполнения.

Макрос получает код как данные, обрабатывает его и возвращает новый код, который затем вычисляется.

3.1. defmacro

Определяет макрос.

Синтаксис:

(defmacro имя (параметры)
тело...)

Тело должно вернуть S-выражение — сгенерированный код.

Пример простого макроса:

(defmacro when-positive (x &body body)
`(if (> ,x 0)
(progn ,@body)))

Использование:

(when-positive temperature
(print "It's warm!")
(turn-on-fan))

Раскрывается в:

(if (> temperature 0)
(progn
(print "It's warm!")
(turn-on-fan)))

3.2. Кавычки: backquote, comma, comma-at

  • Backquote ` — как quote, но позволяет интерполяцию.
  • Comma , — подставляет значение выражения.
  • Comma-at ,@ — раскрывает список (splicing).

Пример:

(let ((x 1) (y 2) (rest '(3 4)))
`(list ,x ,y ,@rest)) ; → (list 1 2 3 4)

3.3. Гигиена и gensym

Чтобы избежать захвата переменных (variable capture), макросы должны использовать уникальные символы.

Функция (gensym) создаёт новый, уникальный символ при каждом вызове.

Пример безопасного макроса:

(defmacro with-gensyms ((&rest vars) &body body)
`(let ,(mapcar (lambda (v) `(,v (gensym))) vars)
,@body))

(defmacro my-let ((var val) &body body)
(with-gensyms (g)
`(let ((,g ,val))
(let ((,var ,g))
,@body))))

Это гарантирует, что внутренняя переменная g не конфликтует с пользовательским кодом.

3.4. Распространённые встроенные макросы

Common Lisp предоставляет множество макросов, построенных на специальных формах:

  • when — как if без ветки else.
    (when (> x 0) (print x))
  • unless — противоположность when.
    (unless (zerop x) (print (/ 1 x)))
  • case — множественный выбор по значению.
    (case op
    (+ (add a b))
    (- (sub a b))
    (otherwise (error "Unknown op")))
  • multiple-value-bind — привязка нескольких значений.
    (multiple-value-bind (quotient remainder)
    (floor 10 3)
    (list quotient remainder)) ; → (3 1)
  • destructuring-bind — деструктуризация списков или векторов.
    (destructuring-bind (x y z) '(1 2 3)
    (+ x y z)) ; → 6

3.5. Отладка макросов

  • (macroexpand-1 form) — раскрывает макрос один уровень.
  • (macroexpand form) — раскрывает рекурсивно, пока возможно.

Пример:

(macroexpand-1 '(when (> x 0) (print x)))
; → (IF (> X 0) (PROGN (PRINT X)))

Полезно для проверки корректности генерации кода.

3.6. Макросы времени компиляции

Макросы выполняются во время компиляции, поэтому они могут:

  • читать файлы,
  • генерировать код на основе внешних данных,
  • оптимизировать конструкции статически.

Пример: макрос, встраивающий содержимое файла как строку:

(defmacro embed-file (path)
(let ((content (uiop:read-file-string path)))
`(defconstant +embedded-content+ ,content)))

(Здесь используется uiop — портабельная библиотека для Common Lisp.)


Управляющие конструкции: условия, циклы и итерация

1. Условные конструкции

Common Lisp предоставляет несколько уровней условных выражений — от простых до сложных многоуровневых ветвлений.

1.1. if

Базовая двоичная условная конструкция.

(if условие
форма-если-истина
форма-если-ложь)

Ветка «ложь» необязательна. Если она отсутствует и условие ложно, результат — NIL.

1.2. when и unless

Упрощённые формы для одностороннего ветвления.

(when условие
форма1
форма2...)

(unless условие
форма1
форма2...)

Эквивалентны:

(when p body...)(if p (progn body...) nil)
(unless p body...)(if p nil (progn body...))

1.3. cond

Многоусловное ветвление. Каждая ветка — список, начинающийся с условия.

(cond
(условие1 тело1...)
(условие2 тело2...)
...
(t тело-по-умолчанию...))

Выполняется первая ветка с истинным условием. Если ни одно условие не истинно и нет ветки (t ...), результат — NIL.

1.4. case, ecase, ccase

Выбор по значению ключа. Работает с eql-сравнением.

(case ключ
(значение1 тело1...)
((значение2 значение3) тело2...)
(otherwise тело-по-умолчанию))

Пример:

(case op
(+ (add a b))
(- (sub a b))
((*) (mul a b))
(otherwise (error "Unknown operator")))
  • ecase — как case, но вызывает ошибку, если ключ не найден.
  • ccase — интерактивная версия, позволяет исправить ключ во время ошибки.

1.5. typecase, etypecase, ctypecase

Ветвление по типу объекта.

(typecase x
(integer (* x 2))
(string (length x))
(list (mapcar #'1+ x)))

Аналогично:

  • etypecase — ошибка при несовпадении всех типов.
  • ctypecase — интерактивное исправление.

2. Циклы

Common Lisp предлагает как низкоуровневые, так и высокоуровневые средства итерации.

2.1. Макрос loop

Самый мощный и гибкий инструмент итерации. Имеет два синтаксиса:

  • Простой: (loop for var from 1 to 10 do (print var))
  • Расширенный: с аккумуляторами, условиями, параллельными итерациями.
Основные конструкции loop:
  • Итерация по числам:

    (loop for i from 1 to 5 do (print i))
    (loop for i from 10 downto 1 by 2 collect i) ; → (10 8 6 4 2)
  • Итерация по списку:

    (loop for x in '(a b c) do (print x))
    (loop for x in '(1 2 3) collect (* x x)) ; → (1 4 9)
  • Итерация по вектору или строке:

    (loop for x across #(1 2 3) sum x)             ; → 6
    (loop for c across "hello" collect c) ; → (#\h #\e #\l #\l #\o)
  • Параллельные итерации:

    (loop for x in '(1 2 3)
    for y in '(a b c)
    collect (list x y)) ; → ((1 A) (2 B) (3 C))
  • Аккумуляторы:

    • collect — собирает значения в список.
    • append — объединяет списки.
    • sum — суммирует числа.
    • count — подсчитывает истинные значения.
    • maximize, minimize — находят экстремумы.
  • Условия:

    (loop for x in '(1 2 3 4 5)
    when (evenp x)
    collect x) ; → (2 4)
  • Завершение:

    (loop for i from 1
    while (< i 10)
    do (print i))
  • Возврат результата:

    (loop for x in list
    if (plusp x) sum x into pos-sum
    else sum x into neg-sum
    finally (return (list pos-sum neg-sum)))

Макрос loop стандартизирован в ANSI Common Lisp и поддерживается всеми реализациями.

2.2. do и do*

Низкоуровневые циклы с явным управлением переменными.

Синтаксис:

(do ((var1 init1 step1)
(var2 init2 step2)
...)
(конечное-условие результат...)
тело...)
  • do — шаговые выражения вычисляются параллельно.
  • do* — шаговые выражения вычисляются последовательно.

Пример факториала:

(defun factorial (n)
(do ((i n (- i 1))
(acc 1 (* acc i)))
((<= i 1) acc)))

2.3. Специализированные итерационные макросы

dolist

Итерация по списку.

(dolist (var list [результат])
тело...)

Пример:

(dolist (x '(1 2 3))
(print (* x x)))

Если указан результат, он возвращается после завершения:

(dolist (x '(1 2 3) 'done)
(print x)) ; печатает 1, 2, 3 → возвращает DONE
dotimes

Итерация по целым числам от 0 до N-1.

(dotimes (var count [результат])
тело...)

Пример:

(dotimes (i 5)
(print i)) ; 0, 1, 2, 3, 4
do-symbols, do-external-symbols, do-all-symbols

Итерация по символам пакета.

(do-symbols (sym (find-package "CL"))
(when (fboundp sym)
(print sym)))
with-package-iterator

Низкоуровневый итератор по символам пакета с фильтрацией.


3. Рекурсия против итерации

Lisp поддерживает оба подхода. Выбор зависит от задачи:

  • Рекурсия — естественна для древовидных структур, обработки вложенных списков, символьных вычислений.
  • Итерация — предпочтительна для производительности, особенно при работе с большими линейными структурами.

Хвостовая рекурсия может быть оптимизирована в цикл компилятором (например, в SBCL), но это не гарантируется стандартом.

Пример: обход дерева

(defun tree-sum (tree)
(cond
((null tree) 0)
((numberp tree) tree)
(t (+ (tree-sum (car tree))
(tree-sum (cdr tree))))))

Тот же алгоритм через стек и итерацию будет быстрее на больших деревьях, но сложнее в написании.


4. Выход из циклов

  • return — выходит из ближайшего block (все циклы создают неявный блок).
  • return-from имя — выходит из именованного блока.
  • loop-finish — специальная форма для немедленного завершения loop.

Пример:

(loop for x in '(1 2 3 4 5)
when (> x 3)
return x) ; → 4

5. Производительность итерации

  • loop часто компилируется в эффективный код.
  • mapcar создаёт новый список — не подходит для больших данных без необходимости копирования.
  • dolist и dotimes — минимальные накладные расходы.
  • Для массивов предпочтительны loop ... across или aref вручную.

Обработка ошибок, условия и система рестартов

Common Lisp обладает одной из самых продвинутых систем обработки исключительных ситуаций среди всех языков программирования — системой условий (Condition System). Она не ограничивается простым «выбросом и перехватом» ошибок, а предоставляет мощные средства для восстановления, перезапуска и интерактивного исправления состояния программы.

1. Условия (Conditions)

Условие — это объект, представляющий исключительную или просто значимую ситуацию: ошибку, предупреждение, информационное сообщение и т.д.

Условия создаются с помощью функции error, warn, signal и других.

1.1. Типы условий

Все условия наследуются от базового типа condition.

Основные подтипы:

  • serious-condition — серьёзные ситуации, требующие обработки.
    • error — фатальные ошибки.
      • simple-error
      • arithmetic-error, division-by-zero, overflow-error
      • control-error, parse-error, file-error, stream-error
    • warning — предупреждения.
      • simple-warning
    • storage-condition — нехватка памяти.
    • program-error — ошибки в коде (например, неверное количество аргументов).

Пользовательские условия определяются через define-condition.

Пример:

(define-condition invalid-age (error)
((age :initarg :age :reader invalid-age-value))
(:report (lambda (c stream)
(format stream "Invalid age: ~A" (invalid-age-value c)))))

2. Сигнализация условий

2.1. error

Сигнализирует ошибку и требует обработки. Если никто не обрабатывает — вызывается отладчик.

(error "Something went wrong")
(error 'division-by-zero :operation '/ :operands '(10 0))
(error 'invalid-age :age -5)

2.2. warn

Сигнализирует предупреждение. Программа продолжает выполнение.

(warn "This is deprecated")

2.3. signal

Сигнализирует условие, но не требует обработки. Используется для информационных или логгинг-событий.

(signal 'my-custom-condition :data some-value)

3. Обработка условий

3.1. handler-case

Перехватывает условия по типу, аналогично try/catch.

Синтаксис:

(handler-case выражение
(тип-условия (переменные...) тело...)
...)

Пример:

(handler-case (/ 10 0)
(division-by-zero () "Cannot divide by zero"))
; → "Cannot divide by zero"

Можно получать доступ к полям условия:

(handler-case (open "missing.txt")
(file-error (c)
(format nil "File error: ~A" (file-error-pathname c))))

3.2. ignore-errors

Специальный макрос, который перехватывает любые ошибки и возвращает два значения:

  • результат (или NIL при ошибке),
  • условие (или NIL, если ошибки не было).

Пример:

(multiple-value-bind (result error)
(ignore-errors (/ 10 0))
(if error
"Failed"
result))
; → "Failed"

3.3. handler-bind

Более гибкий механизм, чем handler-case. Устанавливает динамические обработчики, которые могут:

  • просто записать лог,
  • вызвать рестарт,
  • продолжить выполнение.

Обработчики работают в динамическом окружении, а не как стек исключений.

Синтаксис:

(handler-bind ((условие обработчик) ...)
тело...)

Пример:

(handler-bind ((division-by-zero
(lambda (c)
(declare (ignore c))
(invoke-restart 'use-value 1))))
(/ 10 0))

Этот пример работает только если где-то определён рестарт use-value.

4. Рестарты (Restarts)

Рестарт — это точка восстановления, предлагающая способ продолжить выполнение после ошибки.

Рестарты не прерывают поток управления — они просто становятся доступны при сигнализации условия.

4.1. restart-case

Определяет возможные рестарты в текущем контексте.

Синтаксис:

(restart-case выражение
(имя-рестарта (аргументы...) тело...)
...)

Пример:

(defun safe-divide (a b)
(restart-case
(if (zerop b)
(error "Division by zero")
(/ a b))
(use-value (value)
:report "Use specified value instead"
value)
(retry-with (new-b)
:report "Retry with new divisor"
(safe-divide a new-b))))

Теперь при вызове (safe-divide 10 0) можно выбрать:

  • вернуть произвольное значение,
  • повторить деление с новым делителем.

4.2. with-simple-restart

Упрощённая форма для одного рестарта.

(with-simple-restart (continue "Skip this step")
(some-risky-operation))

4.3. Вызов рестартов

  • invoke-restart — вызывает рестарт по имени.
  • find-restart — ищет рестарт в текущем динамическом окружении.
  • compute-restarts — возвращает список всех доступных рестартов.

Пример автоматического выбора:

(handler-bind ((error
(lambda (c)
(declare (ignore c))
(let ((r (find-restart 'use-value)))
(when r
(invoke-restart r 42))))))
(safe-divide 10 0))
; → 42

5. Интерактивность и отладчик

Если условие не обработано, Common Lisp запускает отладчик, который:

  • показывает стек вызовов,
  • выводит доступные рестарты,
  • позволяет интерактивно выбрать действие.

Это особенно полезно при разработке — можно исправить данные «на лету» и продолжить.

6. Практические шаблоны

6.1. Защита от ошибок с возвратом значения по умолчанию

(defun safe-read-file (path &optional (default ""))
(handler-case
(uiop:read-file-string path)
(file-error () default)))

6.2. Логирование предупреждений

(handler-bind ((warning
(lambda (w)
(log-message "Warning: ~A" w)
(muffle-warning w)))) ; подавляет дальнейшее распространение
(some-operation))

6.3. Автоматический повтор при временной ошибке

(defun retry-on-error (fn max-retries)
(loop for attempt from 1 to max-retries
do (restart-case
(return (funcall fn))
(retry ()
:report "Retry the operation"
(if (= attempt max-retries)
(error "Max retries exceeded")
;; просто продолжаем цикл
)))))

Объектная система CLOS (Common Lisp Object System)

CLOS — это мощная, динамическая и стандартизированная объектно-ориентированная система в Common Lisp. Она поддерживает множественное наследование, обобщённые функции, методы специализации по нескольким аргументам, метаклассы и динамическую модификацию классов во время выполнения.

1. Классы

Классы определяются с помощью defclass.

Синтаксис:

(defclass имя (суперклассы...)
((слот1 :initarg :arg1 :accessor acc1 :initform значение :type тип ...)
...)
(:метакласс имя)
(:documentation "Описание"))

1.1. Слоты (слоты = поля)

Каждый слот может иметь следующие опции:

  • :initarg — ключевое слово для передачи значения при создании экземпляра.
  • :accessor — автоматически создаёт геттер и сеттер.
  • :reader — только геттер.
  • :writer — только сеттер.
  • :initform — значение по умолчанию.
  • :type — ограничение типа (не проверяется строго во всех реализациях).
  • :allocation:instance (по умолчанию) или :class (статический слот, общий для всех экземпляров).

Пример:

(defclass person ()
((name :initarg :name :accessor name :initform "Anonymous")
(age :initarg :age :accessor age :initform 0)
(id :reader id :initform (gensym))))

2. Создание экземпляров

Экземпляры создаются функцией make-instance.

(defvar p (make-instance 'person :name "Alice" :age 30))
(name p) ; → "Alice"
(age p) ; → 30

Если не указан :initarg, слот получает значение из :initform.

3. Обобщённые функции и методы

В CLOS методы не принадлежат классам. Вместо этого определяются обобщённые функции, которые собирают методы по сигнатурам аргументов.

3.1. Определение обобщённой функции

Можно определить без тела — только для документации:

(defgeneric describe-object (obj)
(:documentation "Returns a description of the object."))

3.2. Определение метода

Методы определяются через defmethod.

(defmethod describe-object ((p person))
(format nil "Person ~A, age ~A" (name p) (age p)))

Здесь (p person)специализированный параметр: метод вызывается, если первый аргумент является экземпляром person.

3.3. Множественная диспетчеризация

Метод может специализироваться по нескольким аргументам.

(defclass document () ())
(defclass printer () ())

(defmethod print-document ((doc document) (prn printer))
(format t "Printing document on printer~%"))

При вызове (print-document doc prn) выбирается метод на основе типов обоих аргументов.

4. Наследование

Класс может наследовать от нескольких суперклассов.

(defclass employee (person)
((salary :initarg :salary :accessor salary)))

(defclass student (person)
((university :initarg :university :accessor university)))

(defclass intern (employee student)
((mentor :initarg :mentor :accessor mentor)))

Порядок линеаризации (C3 linearization) определяет порядок поиска слотов и методов.

5. Инициализация: initialize-instance и shared-initialize

Методы инициализации позволяют выполнять действия при создании объекта.

  • initialize-instance — вызывается после выделения памяти.
  • shared-initialize — вызывается как при создании, так и при reinitialize-instance.

Пример:

(defmethod initialize-instance :after ((p person) &key)
(format t "Created person: ~A~%" (name p)))

:after означает, что метод выполняется после стандартной инициализации.

Другие квалификаторы:

  • :before — до основного метода,
  • :around — оборачивает всё (может решить, вызывать ли остальное).

6. Изменение классов во время выполнения

CLOS позволяет динамически изменять структуру класса.

(defclass person () ((name :accessor name)))
(defvar p (make-instance 'person))
(setf (name p) "Bob")

;; Добавляем слот age
(defclass person () ((name :accessor name) (age :accessor age :initform 0)))

В большинстве реализаций существующие экземпляры автоматически обновляются — новые слоты получают :initform.

Функция change-class позволяет изменить класс конкретного экземпляра:

(change-class p 'employee)

7. Интроспекция классов

CLOS предоставляет богатые средства анализа:

  • (class-of obj) — класс объекта.
  • (find-class 'имя) — объект класса по имени.
  • (slot-exists-p obj 'слот) — существует ли слот.
  • (slot-boundp obj 'слот) — привязано ли значение к слоту.
  • (describe obj) — подробная информация.

Пример:

(slot-exists-p p 'name)  ; → T
(slot-boundp p 'age) ; → T (если был задан или есть :initform)

8. Метаклассы

Классы сами являются объектами, и их поведение определяется метаклассами.

Стандартный метакласс — standard-class.

Можно определить собственный метакласс, наследуя от standard-class, чтобы изменить:

  • способ хранения слотов,
  • поведение при создании,
  • правила наследования.

Пример (редко используется):

(defclass singleton-class (standard-class)
())

(defclass my-singleton ()
()
(:metaclass singleton-class))

Затем можно переопределить allocate-instance, чтобы обеспечить единственность экземпляра.

9. Практические рекомендации

  • Используйте :accessor, а не отдельные :reader/:writer, если нужен и геттер, и сеттер.
  • Избегайте глубоких иерархий — CLOS лучше работает с плоскими, композиционными структурами.
  • Предпочитайте обобщённые функции прямому доступу к слотам — это даёт гибкость.
  • Используйте :after/:before методы для логирования, валидации, кэширования.

10. Пример: полиморфный рендеринг

(defclass shape () ())

(defclass circle (shape)
((radius :initarg :radius :accessor radius)))

(defclass rectangle (shape)
((width :initarg :width :accessor width)
(height :initarg :height :accessor height)))

(defgeneric area (shape))

(defmethod area ((c circle))
(* pi (expt (radius c) 2)))

(defmethod area ((r rectangle))
(* (width r) (height r)))

;; Использование
(let ((shapes (list (make-instance 'circle :radius 2)
(make-instance 'rectangle :width 3 :height 4))))
(mapcar #'area shapes)) ; → (12.566... 12)

Компиляция, загрузка, модули и системы сборки

Common Lisp предоставляет мощные средства для организации кода в проекты, управления зависимостями, компиляции и распространения программ. Основной инструмент современной экосистемы — ASDF (Another System Definition Facility).

1. Файлы и загрузка

1.1. Загрузка исходного кода

Функция (load "файл") читает и выполняет файл с расширением .lisp.

  • Если указано только имя без расширения, интерпретатор ищет .lisp, .lsp, .cl и другие варианты.
  • load может загружать как исходный, так и скомпилированный (.fasl, .fasb, .dx64fsl и т.д.) код.

Пример:

(load "my-program.lisp")

1.2. Компиляция

Компиляция ускоряет выполнение и позволяет создавать автономные образы.

Основные функции:

  • (compile-file "input.lisp") — компилирует файл в машинный код, создаёт .fasl.
  • (load "input.fasl") — загружает скомпилированный файл (обычно быстрее).
  • (compile 'имя-функции) — компилирует отдельную функцию.

Пример:

(compile-file "utils.lisp")  ; → "utils.fasl"
(load "utils.fasl")

Большинство реализаций (SBCL, CCL) автоматически компилируют функции при определении в REPL, но явная компиляция нужна для финальной сборки.

2. Пакеты (Packages)

Пакет — это пространство имён для символов. Он решает проблему конфликтов имён между библиотеками.

2.1. Создание пакета

(defpackage :my-app
(:use :cl) ; импортировать все символы из CL
(:export :main :process-data)
(:shadowing-import-from :other-lib :map))
  • :use — наследует символы из другого пакета.
  • :export — делает символы доступными другим.
  • :import-from — импортирует конкретные символы.
  • :shadowing-import-from — импортирует с перекрытием локальных.

2.2. Переключение контекста

(in-package :my-app)

Все последующие определения принадлежат пакету my-app.

2.3. Доступ к символам других пакетов

  • other::internal-symbol — внутренний символ (не экспортирован).
  • other:public-symbol — экспортированный символ.

3. ASDF — система определения проектов

ASDF — стандарт де-факто для управления проектами в Common Lisp.

3.1. Структура проекта

Типичная структура:

my-project/
├── my-project.asd
├── src/
│ ├── main.lisp
│ └── utils.lisp
└── README.md

3.2. Файл .asd

Определяет систему:

(defsystem "my-project"
:version "0.1.0"
:author "Author <author@example.com>"
:license "MIT"
:description "A sample project"
:depends-on ("alexandria" "uiop")
:components ((:module "src"
:components
((:file "utils")
(:file "main" :depends-on ("utils")))))
:build-operation "program-op"
:entry-point "my-project:main")
  • :depends-on — список зависимостей (библиотек из Quicklisp или локальных систем).
  • :components — файлы и модули.
  • :entry-point — функция для запуска (при создании исполняемого файла).

3.3. Загрузка системы

В REPL:

(asdf:load-system "my-project")

Или через Quicklisp:

(ql:quickload "my-project")

4. Quicklisp — менеджер пакетов

Quicklisp — централизованный каталог библиотек.

Установка:

(quickload "drakma")  ; HTTP-клиент
(quickload "cl-ppcre") ; регулярные выражения

Quicklisp автоматически скачивает зависимости и добавляет их в asdf:*central-registry*.

5. Создание исполняемых файлов

Некоторые реализации позволяют создавать standalone-программы.

5.1. SBCL

(sb-ext:save-lisp-and-die "my-app"
:executable t
:toplevel #'my-project:main)

Результат — исполняемый двоичный файл, не требующий Lisp-окружения.

5.2. CCL

(ccl:save-application "my-app" :toplevel-function #'my-project:main)

6. Управление путями и портабельность

Библиотека UIOP (Universal Implementation-dependent Operations Platform) входит в большинство реализаций и обеспечивает:

  • Портабельную работу с файлами: (uiop:pathname-directory-pathname *load-pathname*)
  • Чтение файлов: (uiop:read-file-string "data.txt")
  • Запуск внешних процессов: (uiop:run-program '("ls" "-l"))
  • Обработка аргументов командной строки: (uiop:command-line-arguments)

Пример портабельной загрузки ресурсов:

(defun resource-path (name)
(merge-pathnames name (uiop:pathname-directory-pathname *load-pathname*)))

(uiop:read-file-string (resource-path "config.json"))

7. Сборка и развёртывание

Процесс типичной сборки:

  1. Установить зависимости через Quicklisp.
  2. Загрузить систему: (asdf:load-system "my-project").
  3. Протестировать.
  4. Скомпилировать: (asdf:compile-system "my-project").
  5. Создать исполняемый файл (если нужно).

Для CI/CD можно использовать скрипты на Bash или Makefile:

build:
sbcl --noinform --eval "(ql:quickload :my-project)" \
--eval "(sb-ext:save-lisp-and-die \"my-app\" :executable t :toplevel #'my-project:main)" \
--quit

8. Стандартные соглашения

  • Имя системы совпадает с именем основного пакета.
  • Все файлы кода — в подкаталоге src/.
  • Тесты — в t/ или test/, как отдельная система "my-project/test".
  • Документация — в docs/ или в виде строк документации внутри кода.

9. Отладка загрузки

Если система не загружается:

  • Проверьте, что .asd файл находится в пути, известном ASDF (asdf:*central-registry*).
  • Используйте (asdf:system-relative-pathname "my-project" "src/main.lisp") для относительных путей.
  • Включите логгинг: (setf asdf:*verbose-out* *standard-output*).

Системные переменные, параметры окружения, отладка и интроспекция

Common Lisp предоставляет богатый набор динамических переменных, системных параметров и инструментов интроспекции, позволяющих управлять поведением программы, анализировать состояние выполнения и настраивать взаимодействие с окружением.

1. Специальные (динамические) переменные

В Common Lisp есть два типа переменных:

  • Лексические — привязки через let, видны только в своём блоке.
  • Специальные (динамические) — имеют динамическую область видимости, передаются «вниз» по стеку вызовов.

Глобальные переменные, определённые через defvar или defparameter, являются специальными.

1.1. defvar и defparameter

  • (defvar *имя* значение) — инициализирует переменную только если она не определена. Используется для конфигурационных параметров.
  • (defparameter *имя* значение) — всегда устанавливает значение. Используется для значений по умолчанию.

По соглашению, имена специальных переменных окружаются звёздочками: *standard-output*, *debug-io*.

Пример:

(defparameter *max-retries* 3)
(defvar *log-level* :info)

1.2. Динамическое переназначение

Специальные переменные можно временно переопределять через let:

(let ((*standard-output* (make-string-output-stream)))
(print "Hello")
(get-output-stream-string *standard-output*)) ; → "Hello"

Это позволяет перенаправлять вывод, логирование, поведение функций без изменения их кода.

2. Основные системные переменные

ПеременнаяНазначение
*standard-input*Поток ввода по умолчанию
*standard-output*Поток вывода по умолчанию
*error-output*Поток для ошибок
*debug-io*Двуликий поток (ввод/вывод) для отладчика
*query-io*Поток для интерактивных запросов
*package*Текущий пакет
*readtable*Таблица чтения (управляет синтаксисом)
*features*Список фич реализации (для условной компиляции)
*modules*Список загруженных модулей
*load-pathname*Путь к файлу, который сейчас загружается
*load-truename*Канонический путь к загружаемому файлу

Пример использования *features*:

#+sbcl (format t "Running on SBCL~%")
#+ccl (format t "Running on Clozure CL~%")

Конструкции #+ и #- позволяют включать/исключать код на этапе чтения в зависимости от фич.

3. Отладка

3.1. trace и untrace

Включает трассировку вызовов функции:

(trace factorial)
(factorial 5)
;; Вывод:
;; 1. Trace: (FACTORIAL '5)
;; 2. Trace: (FACTORIAL '4)
;; ...
;; 2. Trace: FACTORIAL ==> 24
;; 1. Trace: FACTORIAL ==> 120

Отключение:

(untrace factorial)

Можно трассировать несколько функций: (trace f1 f2 f3).

3.2. break

Останавливает выполнение и запускает отладчик:

(defun risky-fn (x)
(when (< x 0) (break "Negative input!"))
(* x x))

3.3. inspect

Открывает интерактивный инспектор для объекта:

(inspect my-object)

Позволяет просматривать слоты, внутреннюю структуру, ссылки.

3.4. describe

Выводит информацию об объекте в поток:

(describe *standard-output*)
; → #<SYNONYM-STREAM to *TERMINAL-IO*>

Работает с функциями, классами, пакетами, потоками.

4. Профилирование и измерение времени

4.1. time

Измеряет время выполнения, количество выделенной памяти, сборок мусора:

(time (some-heavy-computation))
; → Evaluation took:
; 0.002 seconds of real time
; 1,000,000 bytes allocated

4.2. Профайлеры реализаций

  • SBCL: (sb-profile:profile функция), затем (sb-profile:report).
  • CCL: встроенный профайлер через (ccl:profile функция).

Пример (SBCL):

(sb-profile:profile "MY-PACKAGE")
(run-benchmark)
(sb-profile:report)
(sb-profile:unprofile)

5. Интроспекция среды выполнения

5.1. Информация о реализации

(lisp-implementation-type)    ; → "SBCL"
(lisp-implementation-version) ; → "2.4.0"
(machine-type) ; → "X86-64"
(machine-instance) ; → "my-pc"
(short-site-name) ; → "LOCAL"

5.2. Управление памятью

  • (room) — выводит статистику по использованию памяти.
  • (gc) — принудительный запуск сборщика мусора.
  • (room t) — подробная разбивка по областям памяти.

5.3. Анализ вызовов

Функция (backtrace) или (print-backtrace) (в зависимости от реализации) выводит стек вызовов.

В SBCL:

(sb-debug:backtrace)

6. Настройка поведения системы

6.1. Уровень отладки

Некоторые реализации используют специальные переменные:

  • SBCL: *efficiency-note-cost-threshold*, *efficiency-note-limit*
  • CCL: ccl:*fasl-save-definitions*

6.2. Компиляторные параметры

Через декларации можно влиять на оптимизацию:

(declaim (optimize (speed 3) (safety 1) (debug 0)))

Уровни от 0 до 3:

  • speed — скорость выполнения,
  • safety — проверки во время выполнения,
  • debug — информация для отладчика,
  • space — экономия памяти.

Пример безопасного режима:

(declaim (optimize (safety 3) (debug 3)))

7. Работа с окружением операционной системы

7.1. Переменные окружения

Через UIOP:

(uiop:getenv "HOME")        ; → "/home/user"
(uiop:setenv "MY_VAR" "42") ; устанавливает переменную

7.2. Аргументы командной строки

(uiop:command-line-arguments)  ; → ("--verbose" "input.txt")

7.3. Текущий рабочий каталог

(uiop:getcwd)                ; → #P"/home/user/project/"
(uiop:chdir #P"/tmp/") ; изменить каталог

8. Практические шаблоны

8.1. Временное перенаправление вывода

(with-output-to-string (s)
(let ((*standard-output* s))
(run-logging-function))
s) ; возвращает строку с логом

8.2. Условная компиляция под реализацию

#+sbcl
(defun get-threads ()
(sb-thread:all-threads))

#+ccl
(defun get-threads ()
(ccl:all-processes))

8.3. Безопасное выполнение с логированием ошибок

(handler-bind ((error (lambda (c)
(format *error-output* "Error: ~A~%" c)
(muffle-warning c))))
(risky-operation))

Практические идиомы, стиль кода и распространённые шаблоны

Эта заключительная часть охватывает рекомендации по написанию чистого, эффективного и поддерживаемого кода на Common Lisp. Она включает соглашения об именовании, организацию проектов, безопасное использование макросов, производительность и типичные паттерны проектирования.

1. Соглашения об именовании

Common Lisp использует kebab-case (слова через дефис) для имён функций, переменных и классов.

  • Функции и переменные: process-data, max-retries, *debug-mode*
  • Предикаты (возвращают логическое значение): заканчиваются на p или -p
    listp, evenp, file-exists-p
  • Деструктивные функции (изменяют аргументы): заканчиваются на r
    nconc, sort (деструктивен), nreverse
  • Макросы: без специального суффикса, но часто выражают управляющие конструкции
    with-open-file, when, unless

Специальные (динамические) переменные окружены звёздочками:
*standard-output*, *features*, *my-config*

Внутренние (приватные) символы не экспортируются из пакета. Их можно помечать одним подчёркиванием в начале, хотя это не обязательно: _helper-fn.

2. Организация кода

2.1. Модульность

  • Каждый файл — одна логическая единица: класс, набор функций, DSL.
  • Избегайте «мусорных» файлов с разнородным кодом.
  • Используйте defpackage в отдельном файле или в начале основного файла модуля.

2.2. Зависимости

  • Минимизируйте зависимости между модулями.
  • Используйте :use только для :cl и, возможно, одной вспомогательной библиотеки.
  • Предпочитайте :import-from явному указанию префиксов (alexandria:flatten).

2.3. Документация

  • Все публичные функции — с (documentation 'fn 'function).
  • Используйте строки документации в defclass, defgeneric, defsystem.
  • Пример:
    (defun safe-divide (a b)
    "Divides A by B. Returns NIL if B is zero."
    (if (zerop b) nil (/ a b)))

3. Безопасные макросы

Макросы — мощный инструмент, но требуют дисциплины.

3.1. Гигиена

  • Всегда используйте gensym для внутренних переменных.
  • Не захватывайте пользовательские символы.

Плохо:

(defmacro bad-let ((var val) &body body)
`(let ((temp ,val))
(let ((,var temp)) ,@body))) ; конфликт, если пользователь использует TEMP

Хорошо:

(defmacro good-let ((var val) &body body)
(let ((g (gensym "VAL")))
`(let ((,g ,val))
(let ((,var ,g)) ,@body))))

3.2. Чёткая семантика

  • Макрос должен вести себя как встроенная форма языка.
  • Избегайте побочных эффектов в аргументах (вычисляйте их один раз).
  • Предоставляйте :report для рестартов.

3.3. Тестирование макросов

  • Тестируйте раскрытие через macroexpand-1.
  • Проверяйте поведение в контексте: локальные переменные, возврат значений.

4. Производительность

4.1. Выбор структур данных

  • Для частого доступа по индексу — векторы (aref O(1)).
  • Для последовательного обхода — списки.
  • Для поиска по ключу — хэш-таблицы (gethash O(1) в среднем).

4.2. Избегание аллокаций

  • Переиспользуйте временные структуры через :fill-pointer и vector-push-extend.
  • Используйте with-output-to-string вместо конкатенации строк в цикле.

4.3. Компиляция

  • Всегда компилируйте перед релизом: (asdf:compile-system "my-app").
  • Указывайте декларации типов для критичных участков:
    (declaim (ftype (function (fixnum fixnum) fixnum) fast-add))
    (defun fast-add (a b)
    (declare (fixnum a b))
    (+ a b))

4.4. Профилирование

  • Не оптимизируйте без замеров.
  • Используйте time, sb-profile, ccl:profile.

5. Распространённые шаблоны

5.1. with- макросы для управления ресурсами

Шаблон обеспечивает автоматическую очистку:

(defmacro with-open-file-safe ((var path) &body body)
`(with-open-file (,var ,path :if-does-not-exist nil)
(when ,var
,@body)))

Аналогично: with-hash-table-iterator, with-slots, with-accessors.

5.2. Аккумуляторы

Для сбора данных в цикле:

(let ((result '()))
(dolist (x data)
(when (validp x)
(push x result)))
(nreverse result))

Или через loop:

(loop for x in data
when (validp x)
collect x)

5.3. Обработка опций через ключевые аргументы

Гибкий API:

(defun render (template &key (output *standard-output*) (escape t) (context nil))
...)

Позволяет вызывать как (render tmpl :escape nil) без указания остальных.

5.4. Использование CLOS для стратегий

Вместо длинных cond — полиморфизм:

(defgeneric validate (validator value))

(defmethod validate ((v email-validator) value)
(cl-ppcre:scan "^[\\w.-]+@[\\w.-]+$" value))

(defmethod validate ((v phone-validator) value)
(cl-ppcre:scan "^\\+7\\d{10}$" value))

6. Антишаблоны

6.1. Избыточное использование eval

  • eval медленный, небезопасный, ломает компиляцию.
  • Почти всегда можно заменить на макрос или замыкание.

6.2. Глобальные состояния без необходимости

  • Избегайте глобальных переменных для передачи данных между функциями.
  • Передавайте явно или используйте динамические привязки только для конфигурации.

6.3. Рекурсия без аккумулятора на больших данных

  • Может вызвать переполнение стека.
  • Используйте итерацию или хвостовую рекурсию с аккумулятором.

6.4. Неправильное сравнение

  • Для чисел — =, для общих объектов — eql, equal, equalp.
  • eq — только для символов и cons-ячеек.
  • Сравнение строк — string=, а не equal.

7. Инструменты для поддержки качества

  • SLIME (Superior Lisp Interaction Mode for Emacs) — лучшая среда разработки.
  • sly — альтернатива SLIME для современных Emacs.
  • clippy или lisp-lint — статический анализ (ограничен).
  • FiveAM, Prove — фреймворки для тестирования.

Пример теста на FiveAM:

(def-suite math-tests)
(in-suite math-tests)

(test square-test
(is (= (square 5) 25))
(is (= (square -3) 9)))

Запуск:

(run! 'math-tests)